<?php
namespace Services\Request;
use Services\Ai\AiService;
use Services\Log\LogService;
use Services\Request\Exceptions\RequestException;
use Think\Log;
use GuzzleHttp\Psr7\Utils;

/**
 * 扣子智能体服务
 */
class CozeRequest extends BaseRequest
{
    const CHAT_PROGRESS = 'in_progress';

    const RETRIEVE_COMPLETED = 'completed';
    const RETRIEVE_FAILED = 'failed';
    /**
     * 接口基础url
     * 若实现了setBaseUri()方法,此处可不定义,两者二选一即可
     * @var string
     */
    public  $baseUri;

    /**
     * 自定义业务侧响应成功状态码
     * 默认0表示成功,非0则失败
     * 可根据对接的三方情况相互约定成功响应的code值
     * @var int
     */
    public  $successCode = 0;

    /**
     * 请求超时时间,单位:s
     * @var int
     */
    public  $timeout = 10;

    /**
     * 是否开启调试 true:开启 false:关闭
     * @var bool
     */
    public  $debug = false;

    /**
     * 是否自动加签, true:是 false:否
     * 开启的话,接口请求默认会追加sign参数进行透传
     * @var bool
     */
    public  $sign = false;

    /**
     * 智能体的配置
     * @var array
     */
    public  $config;

    //access_token缓存key
    const CACHE_BOT_JWT_ACCESS_TOKEN = 'cache_coze_bot_jwt_access_token';
    //access_token缓存过期时间,单位:秒
    const CACHE_BOT_JWT_ACCESS_TOKEN_EXPIRED = 86000;

    //请求接口path
    public const API = [
        'access_token' => '/api/permission/oauth2/token', //获取access_token
        'chat' => '/v3/chat', //发起对话
        'retrieve' => '/v3/chat/retrieve', //查看对话处理状态
        'message' => '/v3/chat/message/list', //获取对话处理结果
        'upload' => '/v1/files/upload', //上传文件
    ];

    public function __construct($config = [])
    {
        $this->config = $config;
        $authType = arr_get($config,'auth_type',AiService::AUTH_PERSONAL);
        if ($authType == AiService::AUTH_PERSONAL) {
            //个人授权
            $accessToken = arr_get($_ENV,'COZE_TOKEN');
        } else {
            //jwt授权
            $accessToken = $this->buildJwtAccessToken($config);
        }
        if (empty($accessToken)) {
            throw new RequestException('请指定智能体访问令牌');
        }
        $this->setHeaders([
            'Authorization' => sprintf(
                'Bearer %s',
                $accessToken
            )
        ]);
    }

    /**
     * 构建API请求的access_token
     * @param $config
     * @return mixed
     */
    public function buildJwtAccessToken($config): mixed
    {
        $cacheKey = self::CACHE_BOT_JWT_ACCESS_TOKEN;
        if (Cache::has($cacheKey)) {
            //如果存在,则直接获取
            $accessToken = Cache::get($cacheKey);
        } else {
            //如果不存在,则请求扣子获取
            $jwt = $this->getJwt(
                $config[VolcanicAgentService::AUTH_WAY_JWT]['kid'],
                $config[VolcanicAgentService::AUTH_WAY_JWT]['iss'],
                $config[VolcanicAgentService::AUTH_WAY_JWT]['privateKey']
            );

            //获取access_token
            $res = $this->accessToken($jwt);

            $accessToken = $res['access_token'];
            Cache::put($cacheKey,$accessToken,self::CACHE_BOT_JWT_ACCESS_TOKEN_EXPIRED);
        }

        return $accessToken;
    }

    /**
     * @param string $kid 应用的公钥指纹,在扣子管理后台扣子API-授权-OAuth应用中查看
     * @param string $iss oauth应用id
     * @param string $privateKeyPath 私钥文件路径
     * @return string
     */
    public function getJwt(string $kid, string $iss, string $privateKeyPath): string
    {
        $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyPath));

        $header = $this->base64URL(json_encode([
            'alg' => 'RS256',
            'typ' => 'JWT',
            'kid' => $kid,
        ]));

        $time = time();
        $payload = $this->base64URL(json_encode([
            'iss' => $iss,
            'aud' => 'api.coze.cn',
            'iat' => $time,
            'exp' => $time + 300,//过期时间
            'jti' => get_sub_sn(),
        ]));

        $data = $header . '.' . $payload;


        $signature = '';
        openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
        $base64UrlSignature = $this->base64URL($signature);
        $jwt = $data . '.' . $base64UrlSignature;

        return $jwt;
    }

    /**
     * 在扣子获取access_token
     * @param $jwt
     * @return mixed
     * @throws \Exception
     */
    public function accessToken($jwt): mixed
    {
        $this->setHeaders([
            'Authorization' => sprintf(
                'Bearer %s',
                $jwt
            )
        ]);
        $params = [
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'duration_seconds' => 86399,//申请的 AccessToken 有效期，单位为秒，默认 900 秒，即 15 分钟。最大可设置为 86399 秒，即 24 小时。
        ];

        return $this->buildRequest(self::API['access_token'], $params, 'json');
    }

    /**
     * 对字符串进行Base64URL编码
     * @param $data
     * @return string
     */
    public function base64URL($data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    /**
     * 获取对话处理结果
     * @param $conversationId
     * @param $chatId
     * @throws \Exception
     */
    public function message($conversationId, $chatId): array
    {
        $params = [
            'conversation_id' => $conversationId,
            'chat_id' => $chatId,
        ];

        return $this->buildRequest(self::API['message'], $params, 'get');
    }

    /**
     * 查看对话处理状态
     * @param $options
     * @throws \Exception
     */
    public function retrieve($options = [])
    {
        $params = [
            'conversation_id' => arr_get($options,'conversation_id'),
            'chat_id' => arr_get($options,'chat_id'),
        ];

        return $this->buildRequest(self::API['retrieve'], $params, 'get');
    }

    /**
     * 发起对话
     * @param $params
     * @throws \Exception
     */
    public function chat($params)
    {
        $stream = arr_get($params, 'stream', false);
        $type = arr_get($params, 'type', '');
        $additionalMessages = [];
        $baseAdditionalMessages = [
            'role' => 'user',
            'content' => $params['content'],
            'content_type' => 'text',
        ];
        //牵扯到上传文件的请求格式不一样
//        if ($type == VolcanicAgentModel::TYPE_PICTURE_LABEL) {
//            $baseAdditionalMessages = [
//                'role' => 'user',
//                'content' => $params['content'],
//                'content_type' => 'object_string',
//            ];
//        }
        $additionalMessages[] = $baseAdditionalMessages;
        $params = [
            'bot_id' => $params['bot_id'],
            'user_id' => time().random_int(1,99999),
            'stream' => $stream,
            'additional_messages' => $additionalMessages,
            'auto_save_history' => true,//必须开启,会影响流式响应最终结果查询
        ];

        //初始化客户端
        $this->setClient([
            'stream' => $params['stream']
        ]);

        return $this->buildRequest(self::API['chat'], $params, 'json');
    }

    /**
     * 定义请求三方接口的基础url
     * @return string
     */
    public function setBaseUri(): string
    {
        return 'https://api.coze.cn';
    }

    /**
     * 上传文件
     * @param $params
     * @return mixed
     * @throws \Exception
     */
    public function upload($params = [])
    {
        $fileUrl = arr_get($params, 'file_url', '');
        $fileName = arr_get($params, 'file_name', '');
        return $this->buildRequest(self::API['upload'], [
            [
                'name' => 'file',
                'contents' => Utils::streamFor(file_get_contents($fileUrl)),
                'filename' => $fileName,
            ]
        ], 'multipart');
    }

    /**
     * 重写父类的响应处理,针对不同的三方,则自定义最终响应结果该如何处理
     * @return void
     * @throws \Exception
     */
    public function parseResponse()
    {
        try {
            //非流式响应
            if (empty($this->clientConfig['stream'])) {

                $result = $this->getContent();
                $errorMsg = '系统繁忙';
                if (!empty($result['code']) && $result['code'] != $this->successCode) {
                    if ($result['code'] == 4014) {
                        if (!empty($result['msg']) && str_contains($result['msg'],'inappropriate')) {
                            $errorMsg = '图片涉及敏感和不当内容,请检查';
                        }
                    }
                    LogService::writeWithDb(['error' => sprintf(
                        '请求智能体服务失败: code: %s, msg: %s',
                        $result['code'],
                        $result['msg'] ?? '未知错误',
                    )],'请求智能体服务失败',Log::ERR);
                    throw new RequestException(sprintf(
                        '请求智能体服务失败: code: %s, msg: %s',
                        $result['code'],
                        $errorMsg
                    ));
                }

                //对话结果的判断
                if($this->getUri() == self::API['chat']){
                    $status = arr_get($result,'data.status');
                    if($status !== self::CHAT_PROGRESS){
                        throw new RequestException('对话任务创建失败');
                    }
                }

                if ($this->getUri() == self::API['access_token']) {
                    $this->setContent($result ?? []);
                } else {
                    $this->setContent($result['data'] ?? []);
                }
            }
        } catch (\Throwable $exception) {
            LogService::writeWithDb([
                'base_uri' => $this->getBaseUri(),
                'url' => $this->getUri(),
                'params' => $this->getParams(),
                'method' => $this->getMethod(),
                'body' => $this->getBody(),
                'content' => $this->getContent(),
                'message' => $exception->getMessage(),
            ],'智能体请求异常');

            throw $exception;
        }
    }
}
